/************************************************************************
Copyright 2003, 2004, 2005, 2006, 2007, 2008 Grid Technology Research Center,
National Institute of Advanced Industrial Science and Technology.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
************************************************************************/

package jp.aist.gtrc.plus.reserve;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Calendar;
import java.util.Collection;
import java.util.LinkedList;

import jp.aist.gtrc.plus.scheduler.node.NodeManager;
import jp.aist.gtrc.plus.scheduler.node.ResourceOption;
import jp.aist.gtrc.plus.scheduler.specific.sge.qbase.SGEQbaseReserveInfo;
import jp.aist.gtrc.plus.scheduler.util.TextUtil;


import org.apache.log4j.Logger;

import com.db4o.Db4o;
import com.db4o.ObjectContainer;
import com.db4o.ObjectSet;
import com.db4o.config.Configuration;
import com.db4o.config.ObjectClass;


public class ReserveRecorderImpl implements ReserveRecorder {

	private final transient ObjectContainer db;
	private transient ReserveOperation ope = null;

	private LastReserveId lastId = new LastReserveId();

	private static final transient String DB_FILE_NAME = "reserves.db4o";
	private static final String OUR_PKG_NAME = "jp.aist.gtrc.plus";
	protected static final transient Logger logger = Logger.getLogger(ReserveRecorderImpl.class);

	{
		assert(ReserveRecorderImpl.class.getCanonicalName().startsWith(OUR_PKG_NAME));
	}

	static class LastReserveId extends ReserveId {

		private static final long serialVersionUID = 4801320709790482891L;
		// db4o classify objects by its class.
		// LastReserveId exists for distinction from all other ReserveId.

		LastReserveId() {
			super(ReserveId.MINGUARD_ID, false);
		}

	}

	private <T> void setCascadeOnDelete(Class<T> c, boolean storeTrans) {

		Configuration conf = Db4o.configure();
		ObjectClass oc = conf.objectClass(c);
		oc.cascadeOnActivate(true);
		oc.cascadeOnDelete(true);
		oc.cascadeOnUpdate(true);
		oc.storeTransientFields(storeTrans);

	}

	private void db4oConfig() {

		Configuration conf = Db4o.configure();
		conf.activationDepth(10);
		conf.exceptionsOnNotStorable(true);
		conf.updateDepth(10);

		/*
		 * If set(ReserveInfo) called, update not only ReserveInfo
		 * but also ReserveStatus, ReserveReqeust, Map, String, ...
		 */
		setCascadeOnDelete(SGEQbaseReserveInfo.class, false);
		setCascadeOnDelete(ReserveInfo.class, false);
		setCascadeOnDelete(ReserveStatus.class, false);
		setCascadeOnDelete(ReserveRequest.class, false);
		setCascadeOnDelete(ReserveNodeSet.class, false);
		setCascadeOnDelete(ResourceOption.class, false);
		setCascadeOnDelete(LastReserveId.class, false);

		/*
		 * We must save transient members of Calendar.
		 * Calendar needs them.
		 */
		setCascadeOnDelete(Calendar.class, true);

		ObjectClass oc;
		oc = conf.objectClass(ReserveOperation.class);
		oc.cascadeOnActivate(false);
		oc.cascadeOnDelete(false);
		oc.cascadeOnUpdate(false);
		oc.storeTransientFields(false);

	}

	public ReserveRecorderImpl(String rsvFilePath) throws ReserveException {

		try {
			// db4oConfig() must be called BEFORE openFile()
			db4oConfig();

			if (TextUtil.isEmpty(rsvFilePath)) {
				rsvFilePath = ".";
			}
			String rsvFile = rsvFilePath + "/" + DB_FILE_NAME;
			db = Db4o.openFile(rsvFile);
			logger.info("Open reserve db file : " + rsvFile);
		} catch (Exception e) {
			/*
			 * NOTE: Some db4o methods without 'throws' keyword throw
			 * RuntimeException, but it really means IOException etc.
			 * We must always use try-catch block when calling db4o
			 * methods even if they don't have 'throws' keyword.
			 */
			logger.error("Cannot open db file", e);
			throw new ReserveException("Cannot open reserve db file");
		}

	}

	public void store(ReserveInfo info) throws ReserveException{

		assert(info != null);

		try {
			// ObjectContainer#store()
			//	newly stores objects or updates stored objects.
			logger.debug("Try store: " + info);
			db.set(info);

			// We want to know last reserve id beyond server restart.
			// lastId exists in DB always even if all reserves are expired.
			ReserveId id = info.getReserveId();
			if (id.compareTo(lastId) > 0) {
				// 'id' may not last if called from reserve modify.
				lastId.setId(id);
				db.set(lastId);
				logger.debug("LastId changed to " + id);
			}
			logger.debug("Stored");
		} catch (Exception e) {
			info.setAsError();
			logger.warn("Store failed", e);
			throw new ReserveException(e.getMessage());
		}

	}

	private void listAllFields(Collection<Object> objList,
			Class<?> targetClass, Object targetObject) {

		if ((targetClass == null) || (targetObject == null)) {
			return;
		}
		assert(objList != null);


		for (Field field : targetClass.getDeclaredFields()) {
			// NOTE: f.getClass() always return java.lang.reflect.Field, use getType()
			Class<?> fieldClass = field.getType();

			int mod = field.getModifiers();
			if (Modifier.isStatic(mod) || Modifier.isTransient(mod)) {
				// db4o doesn't save static/transient fields.
				// It's not necessary to delete them.
				continue;
			}
			if (fieldClass.isEnum() || fieldClass.isPrimitive()
					|| fieldClass == String.class) {
				continue;
			}
			if (fieldClass.isArray()) {
				String cName = fieldClass.getCanonicalName();
				if (cName.contains(".") == false) {
					// skip primitive array such as int[], boolean[] etc.
					continue;
				}
				if (cName.startsWith(String[].class.getCanonicalName())) {
					// skip String[], String[][], ... etc.
					continue;
				}
			}

			Object fieldObject = null;
			try {
				field.setAccessible(true);
				fieldObject = field.get(targetObject);
			} catch (Exception e) {
				assert(false);
				continue;
			}
			if (fieldObject == null) {
				continue;
			}

			objList.add(fieldObject);

			// check objects referenced by this field recursively
			listAllFields(objList, fieldClass, fieldObject);
		}

		Class<?> superClass = targetClass.getSuperclass();
		if ((superClass != null) && (superClass.isEnum() == false)
				&& superClass.getCanonicalName().startsWith(OUR_PKG_NAME)) {
			// check objects referenced by this object
			// as superClass recursively.
			listAllFields(objList, superClass, targetObject);
		}

	}

	private void removeAllRefs(ReserveInfo refObject) throws ReserveException {

		Collection<Object> objList = new LinkedList<Object>();
		objList.add(refObject);
		listAllFields(objList, refObject.getClass(), refObject);

		for (Object obj : objList) {
			db.delete(obj);
		}

	}

	public void remove(ReserveInfo info) throws ReserveException{

		assert(info != null);

		try {
			logger.debug("Try delete: " + info);
			/*
			 * We must delete 'info' and all referenced objects by it
			 * (such as ReserveNodeSet, ReserveId, ResourceOption etc).
			 */
			removeAllRefs(info);
			logger.debug("Deleted: " + info);
		} catch (Exception e) {
			info.setAsError();
			logger.warn("Delete failed", e);
			throw new ReserveException(e.getMessage());
		}

	}

	public void commit(ReserveInfo info) throws ReserveException{

		try {
			db.commit();
			logger.debug("Commited: " + info);
		} catch (Exception e) {
			logger.warn("Commit failed", e);
			throw new ReserveException(e.getMessage());
		}

	}

	public void rollback() throws ReserveException{

		try {
			db.rollback();
			logger.debug("Rollbacked: ");
		} catch (Exception e) {
			logger.warn("Rollback failed", e);
			throw new ReserveException(e.getMessage());
		}

	}

	protected void recoverTransient(ReserveInfo info, NodeManager nodeMgr) {

		info.recoverTransient(nodeMgr);

	}

	private ObjectSet<Object> loadAll()  throws ReserveException {

		try {
			/*
			 * infos is a set of all objects which refered by ReserveInfo.
			 * It has not only ReserveInfo object but also ReserveStatus,
			 * ReserveRequests, Calendar, many Strings, Maps, ..., and so on.
			 * db.get(new ReserveInfo()) returns empty list.
			 */
			return db.get(null);
		} catch (Exception e) {
			logger.warn("Load failed", e);
			throw new ReserveException(e.getMessage());
		}

	}

	public Collection<ReserveInfo> loadAll(NodeManager nodeMgr) throws ReserveException {

		assert(nodeMgr != null);

		ObjectSet<Object> infos = loadAll();
		LinkedList<ReserveInfo> infoList = new LinkedList<ReserveInfo>();
		for (Object obj : infos) {
			if (obj instanceof ReserveInfo) {
				ReserveInfo info = (ReserveInfo)obj;
				recoverTransient(info, nodeMgr);
				infoList.add(info);
			} else if (obj instanceof LastReserveId) {
				lastId = (LastReserveId)obj;
				ReserveId.setLastId(lastId);
				logger.debug("LastId changed to " + lastId.toString());
			} else if (obj instanceof ReserveOperation) {
				ope = (ReserveOperation)obj;
			}
		}

		// NOTE: infoList entries may not sorted by ReserveId
		return infoList;

	}

	public void finish() {

		if (db != null) {
			try {
				db.close();
				logger.debug("Closed DB file");
			} catch (Exception e) {
				// ignore
				logger.warn("Closed DB file failed", e);
			}
		}

	}

	public void storeOperation(ReserveOperation ope) throws ReserveException {

		logger.debug("Try store Operation " + ope);

		try {
			db.set(ope);
		} catch (Exception e) {
			logger.debug("Cannot store ReserveOperation", e);
			throw new ReserveException("Cannot store ReserveOperation");
		}

	}

	public void removeOperation(ReserveOperation ope) throws ReserveException {

		logger.debug("Try remove Operation " + ope);

		try {
			db.delete(ope);
		} catch (Exception e) {
			logger.debug("Cannot remove ReserveOperation", e);
			throw new ReserveException("Cannot remove ReserveOperation");
		}

	}

	public ReserveOperation loadOperation() throws ReserveException {

		if (ope == null) {
			/*
			 * We use 'ope' only, recoverTransient() is needless.
			 */
			ObjectSet<Object> infos = loadAll();
			for (Object obj : infos) {
				if (obj instanceof ReserveOperation) {
					ope = (ReserveOperation)obj;
					break;
				}
			}
		}
		return ope;

	}

}
